{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# CrewAI on VertexAI Reasoning Engine\n",
    "\n",
    "|||\n",
    "|----------|-------------|\n",
    "| Author(s)   | [Christos Aniftos](http://www.github.com/anifort) |\n",
    "| Reviewer(s) | Sokratis Kartakis |\n",
    "| Last updated | 2024 11 14  |\n",
    "| |   |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "<table align=\"left\">\n",
    "  <td style=\"text-align: center\">\n",
    "    <a href=\"https://colab.research.google.com/github/GoogleCloudPlatform/applied-ai-engineering-samples/blob/main/genai-on-vertex-ai/agents/reasoning_engine/crewai/CrewAI_on_VertexAI_Reasoning_Engine.ipynb\">\n",
    "      <img width=\"32px\" src=\"https://www.gstatic.com/pantheon/images/bigquery/welcome_page/colab-logo.svg\" alt=\"Google Colaboratory logo\"><br> Open in Colab\n",
    "    </a>\n",
    "  </td>\n",
    "  <td style=\"text-align: center\">\n",
    "    <a href=\"https://console.cloud.google.com/vertex-ai/colab/import/https:%2F%2Fraw.githubusercontent.com%2FGoogleCloudPlatform%2Fapplied-ai-engineering-samples%2Fmain%2Fgenai-on-vertex-ai%2Fagents%2Freasoning_engine%2Fcrewai%2FCrewAI_on_VertexAI_Reasoning_Engine.ipynb\">\n",
    "      <img width=\"32px\" src=\"https://lh3.googleusercontent.com/JmcxdQi-qOpctIvWKgPtrzZdJJK-J3sWE1RsfjZNwshCFgE_9fULcNpuXYTilIR2hjwN\" alt=\"Google Cloud Colab Enterprise logo\"><br> Open in Colab Enterprise\n",
    "    </a>\n",
    "  </td>\n",
    "  <td style=\"text-align: center\">\n",
    "    <a href=\"https://console.cloud.google.com/vertex-ai/workbench/deploy-notebook?download_url=https://raw.githubusercontent.com/GoogleCloudPlatform/applied-ai-engineering-samples/main/genai-on-vertex-ai/agents/reasoning_engine/crewai/CrewAI_on_VertexAI_Reasoning_Engine.ipynb\">\n",
    "      <img src=\"https://www.gstatic.com/images/branding/gcpiconscolors/vertexai/v1/32px.svg\" alt=\"Vertex AI logo\"><br> Open in Vertex AI Workbench\n",
    "    </a>\n",
    "  </td>\n",
    "  <td style=\"text-align: center\">\n",
    "    <a href=\"https://github.com/GoogleCloudPlatform/applied-ai-engineering-samples/blob/main/genai-on-vertex-ai/agents/reasoning_engine/crewai/CrewAI_on_VertexAI_Reasoning_Engine.ipynb\">\n",
    "      <img width=\"32px\" src=\"https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg\" alt=\"GitHub logo\"><br> View on GitHub\n",
    "    </a>\n",
    "  </td>\n",
    "</table>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "FkxBln-jyxXs"
   },
   "source": [
    "\n",
    "\n",
    "This demo uses the default crewai project skeleton template to allow the use of Gemini model.\n",
    "\n",
    "[CrewAI](https://www.crewai.com/) is an open-source framework designed to make it easier to develop and manage applications that use multiple AI agents working together. Think of it like a team of specialized AI \"workers\" collaborating to achieve a common goal.\n",
    "\n",
    "\n",
    "At the time of the demo creation we used crewai version 0.63.6 and therefore some of the changes we mentioned might be outdate in future versions.\n",
    "\n",
    "We explicetly define library versions in order to avoid breaking this demo in the future.\n",
    "\n",
    "\n",
    "If you want to know more about starting a new CrewAI project from template look here: [Starting Your CrewAI Project](https://docs.crewai.com/getting-started/Start-a-New-CrewAI-Project-Template-Method/) ."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "3wm9RIA-zVXg"
   },
   "source": [
    "### Installing dependencies\n",
    "\n",
    "First we need to install **crewai** which comes with a CLI command to start a new project.\n",
    "Additionally CrewAI is using poetry to manage dependencies.\n",
    "\n",
    "Lets install those 2 packages"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 950
    },
    "id": "p3Zn3Amr2Z0H",
    "outputId": "9f211d78-e392-449f-ae51-5e7b98ddb7fa"
   },
   "outputs": [],
   "source": [
    "!pip install vertexai"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "collapsed": true,
    "id": "UGF-lan3Ck3d",
    "jupyter": {
     "outputs_hidden": true
    },
    "outputId": "b3403f39-b475-4a05-fb85-77b17b2b0d43"
   },
   "outputs": [],
   "source": [
    "!pip install -q 'crewai[tools]==0.63.6' 'poetry'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "JwFbDocwFKBf"
   },
   "source": [
    "Here you can define your CrewAI Project Name."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "EFazK4J5ZSWE"
   },
   "outputs": [],
   "source": [
    "CREWAI_PROJECT_NAME = \"gcp_crewai\"   # @param {type:\"string\"}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "lJ1N2bTav80P"
   },
   "source": [
    "Now lets create a crewai project. The code below makes sure it resets the directory where this notebook runs. Even though the first time running this notebook we will be in the notebooks current path, however in a cell below after we create the crewai project we get into our project directory once that is created.(i.e `CD CREWAI_PROJECT_NAME`). As a result future executions of this notebook need to reset to the default path."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "zE3zE5iYCoWu",
    "outputId": "81320742-2c13-45b8-84fd-edfedf351779"
   },
   "outputs": [],
   "source": [
    "HOME = get_ipython().getoutput('pwd')\n",
    "if (HOME[0].endswith(CREWAI_PROJECT_NAME)):\n",
    "  %cd ..\n",
    "  HOME = get_ipython().getoutput('pwd')\n",
    "\n",
    "!crewai create crew {CREWAI_PROJECT_NAME}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "_3S0voA0FE9p"
   },
   "source": [
    "Okey now that we created our crewai project lets switch directories and get into our project dir.\n",
    "\n",
    "p.s: You can see the created project folder in the file explorer on the left."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "tHF-jCcHDBf_",
    "outputId": "d4c62af6-dee9-4173-b12a-3f83b4ac346c"
   },
   "outputs": [],
   "source": [
    "%cd {HOME[0]}/{CREWAI_PROJECT_NAME}\n",
    "!ls -la"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "km0UX0iQ1J8F"
   },
   "source": [
    "# Install project dependencies"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "Sl9NsoeB6Nzy"
   },
   "source": [
    "The following command will install them in case they did not install during addition"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "collapsed": true,
    "id": "OtATTGmcDs8R",
    "jupyter": {
     "outputs_hidden": true
    },
    "outputId": "853b7cf1-38c2-4d2a-e89d-710c833ac187"
   },
   "outputs": [],
   "source": [
    "!poetry install"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "xdO158fe0M1O"
   },
   "source": [
    "# Initialize the Google Cloud Vertex AI Python SDK\n",
    "\n",
    "### Set Your Project ID, Location and Staging Bucket"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "id": "1qkGHmOzHb7N"
   },
   "outputs": [],
   "source": [
    "PROJECT_ID = \"YOUR_PROJECT_ID_HERE\"  # @param {type:\"string\"}\n",
    "LOCATION = \"us-central1\" # @param {type:\"string\"}\n",
    "STAGING_BUCKET = \"gs://YOUR_STAGING_BUCKET_HERE\" # @param {type:\"string\"}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Create the Bucket if it does not exist:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!set -x && gsutil mb -p $PROJECT_ID -l $LOCATION $STAGING_BUCKET"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "5AeEvkTG0YHa"
   },
   "source": [
    "## Authenticate user\n",
    "\n",
    "The method for authenticating your Google Cloud account is dependent on the environment in which this notebook is being executed. Depending on your Jupyter environment, you may have to manually authenticate.\n",
    "\n",
    "Refer to the subsequent sections for the appropriate procedure.\n",
    "\n",
    "#### **1. For Vertex AI Workbench**\n",
    "-  Do nothing as you are already authenticated.\n",
    "\n",
    "\n",
    "#### **2. Local JupyterLab instance**\n",
    " - Uncomment and run code below:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# !gcloud auth login"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### **3. For Colab (Recommended)**\n",
    "\n",
    "- If you are running this notebook on Google Colab, run the following cell to authenticate your environment."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "aIkHDj6_F0rn",
    "outputId": "831ae22f-4a39-4df9-cb82-dba4a9ba3e10"
   },
   "outputs": [],
   "source": [
    "# Colab authentication - This is to authenticate colab to your account and project.\n",
    "import sys\n",
    "\n",
    "if \"google.colab\" in sys.modules:\n",
    "    from google.colab import auth\n",
    "    auth.authenticate_user(project_id=PROJECT_ID)\n",
    "    print(\"Authenticated\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Set model name according to [litellm syntax](https://docs.litellm.ai/docs/providers/gemini#chat-models)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "02r1HE2aJYXr"
   },
   "outputs": [],
   "source": [
    "MODEL_NAME = \"vertex_ai/gemini-2.0-flash-001\"  # @param {type:\"string\"}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "imJqsjcXKIxj"
   },
   "source": [
    "Now lets see how we can enable Gemini in a CrewAI project. CrewAI uses litellm and we can use the a vertex_ai model name for each of our agents. We need to edit Agent config to change default LLM to Vertex Gemini.\n",
    "\n",
    "here is an example:\n",
    "```python\n",
    "reporting_analyst:\n",
    "  backstory: You're a meticulous analyst with a keen eye for detail. You're known\n",
    "    for your ability to turn complex data into clear and concise reports, making it\n",
    "    easy for others to understand and act on the information you provide.\n",
    "  goal: Create detailed reports based on {topic} data analysis and research findings\n",
    "  llm: vertex_ai/gemini-2.0-flash-001\n",
    "```\n",
    "\n",
    "We can define the LLM by editing the yaml in the editor, however we provide a script that does the same programatically.\n",
    "\n",
    "Feel free to inspect the file under CREWAI_PROJECT_NAME/src/CREWAI_PROJECT_NAME/config/agents.yaml before and after the execution of the cell below"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "A6wxFe6vKGdd",
    "outputId": "aff6758d-5b8b-4359-8c44-82ae392166e0"
   },
   "outputs": [],
   "source": [
    "import yaml\n",
    "\n",
    "agent_yaml = f\"./src/{CREWAI_PROJECT_NAME}/config/agents.yaml\"\n",
    "\n",
    "with open(agent_yaml) as f:\n",
    "     agent_config = yaml.safe_load(f)\n",
    "\n",
    "# This loop removes additional new line characters in the end of a text value\n",
    "for k,v  in agent_config.items():\n",
    "    for attribute,value in v.items():\n",
    "      if value.endswith(\"\\n\"):\n",
    "        v[attribute] = value[:-1]\n",
    "    # for each agent we add a key called llm and the model name of choice.\n",
    "    v['llm']=MODEL_NAME\n",
    "\n",
    "with open(agent_yaml, \"w\") as f:\n",
    "    yaml.dump(agent_config, f)\n",
    "    print(f\"file {agent_yaml} successfully updated!\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "_Uko5otbli7u"
   },
   "source": [
    "### Running our crew demo\n",
    "\n",
    "By default this demo allows you to rin a researhc on a topic of choice using 2 agents, a Senior Data Researcher that runs a research on a given topic and a Reporting Analyst that prepares a report using the findings from the Researcher. \n",
    "\n",
    "Let's test our crew now that we have applied the changes. We will run it locally using the CLI.\n",
    "\n",
    "**Because Agents do multiple calls to the VertexAI Gemini API it is possible that some of the executions will run out of quotas. If you get `RESOURCE_EXHAUSTED` error pause and try again after a minute.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "GLaWkpVCD8Je",
    "outputId": "99fcb822-a283-4239-b6bb-43e99d39bb4b"
   },
   "outputs": [],
   "source": [
    "!poetry run {CREWAI_PROJECT_NAME}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "7DWYBYvrCMqE"
   },
   "source": [
    "### Preapare CrewAI interface for Reasoning Engine\n",
    "\n",
    "Now that we know CrewAI works locally we will go ahead and prepare for reasoning engine deployment.\n",
    "\n",
    "To be able to run CrewAI on Reasoning Engine we need to create a class that defines an `__init__`, `setup` and `query` functions and crew_ai_app.py.\n",
    "\n",
    "Below you can see what we are creating a crew_ai_app.py that can be used as our wrapper for reasoning engine deployment\n",
    "<br /><br />\n",
    "#### Some highlights:\n",
    "\n",
    "* `def set_up(self)`: We define what happens when our application starts. Depending on your implementation here you might want to initialise other libraries, set logging etc. In our simple example we only set the project id as an environment variable to optain the right permissions to resourses.\n",
    "\n",
    "* `CrewaiGcpCrew().crew().kickoff(inputs={\"topic\": question})`: runs the CrewAI for a given topic. The response should be returned as __str__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "EO5Ltljg-aTn"
   },
   "outputs": [],
   "source": [
    "wrapper_file_content = (\"\"\"\n",
    "from src.{PROJECT_NAME}.crew import {CLASS_NAME}Crew as CrewProject\n",
    "from typing import Dict, List, Union\n",
    "import vertexai\n",
    "import os\n",
    "\n",
    "class CrewAIApp:\n",
    "\n",
    "    def __init__(self, project: str, location: str) -> None:\n",
    "        self.project_id = project\n",
    "        self.location = location\n",
    "\n",
    "    def set_up(self) -> None:\n",
    "        os.environ['GOOGLE_CLOUD_PROJECT'] = self.project_id\n",
    "        return\n",
    "\n",
    "    def query(self, question: str) -> Union[str, List[Union[str, Dict]]]:\n",
    "        res = CrewProject().crew().kickoff(inputs={{\"topic\": question}})\n",
    "        return res.__str__()\n",
    "\"\"\").format(PROJECT_NAME=CREWAI_PROJECT_NAME,\n",
    "            CLASS_NAME=''.join(word.title() for word in (CREWAI_PROJECT_NAME.split('_'))))\n",
    "\n",
    "with open(f\"crew_ai_app.py\", \"w\") as f:\n",
    "    f.write(wrapper_file_content)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "XXhjb9TCqOdG"
   },
   "source": [
    "#### Test Wrapper locally\n",
    "Now that we created our wrapper we need to ensure that it can run and trigger crewai."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "UPbtkpYZmyLG",
    "outputId": "66a8b8fc-1e76-47eb-c03a-8b24c244872d"
   },
   "outputs": [],
   "source": [
    "from crew_ai_app import CrewAIApp\n",
    "\n",
    "app = CrewAIApp(project=PROJECT_ID, location=LOCATION)\n",
    "app.set_up()\n",
    "response_c = app.query(\"AI\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "pB4FyJAF3cxx"
   },
   "source": [
    "## Time to initialise VertexAI and deploy our crew to reasoning engine"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "-dA-oLFHpYtg"
   },
   "outputs": [],
   "source": [
    "import vertexai\n",
    "from vertexai.preview import reasoning_engines\n",
    "\n",
    "vertexai.init(project=PROJECT_ID, location=LOCATION, staging_bucket=STAGING_BUCKET)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "i2cDg8aV3olk"
   },
   "source": [
    "Lets see existing engines on our project"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "fV8F7d7NvH95",
    "outputId": "2748bde5-9d52-4fa0-bc5c-3fd3edbc8698"
   },
   "outputs": [],
   "source": [
    "reasoning_engine_list = reasoning_engines.ReasoningEngine.list()\n",
    "print(reasoning_engine_list)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "Xyn1cpSZ3t-i"
   },
   "source": [
    "Reasoning engine instance needs to have the required libraries needed for crewai to execute successfully. As CrewAI uses poetry we will export the dependencies in a requirements.txt and process that to create the necessary reasoning engine requirements list"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "hgsPs1j3zkXH",
    "outputId": "0cdcb276-8f63-4ced-b606-5bfafbf7bb3f"
   },
   "outputs": [],
   "source": [
    "!poetry export --without-hashes --format=requirements.txt > requirements.txt \\\n",
    "# && pip install -r requirements.txt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "EuhP6VkowZRR"
   },
   "outputs": [],
   "source": [
    "with open('./requirements.txt') as f:\n",
    "    requirements = f.read().splitlines()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "eKJanxDr4QtF"
   },
   "source": [
    "## It's deployment time!\n",
    "Deployment takes few minutes. Good time to grap a coffee! &#9749;"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "mx6Cp4oVwDGR",
    "outputId": "49ba196a-a7bd-4996-e33a-013b2864e33f"
   },
   "outputs": [],
   "source": [
    "# Create a remote app with reasoning engine.\n",
    "# This may take few minutes to finish.\n",
    "from crew_ai_app import CrewAIApp\n",
    "\n",
    "reasoning_engine = reasoning_engines.ReasoningEngine.create(\n",
    "    CrewAIApp(project=PROJECT_ID, location=LOCATION),\n",
    "    display_name=\"Demo Addition App\",\n",
    "    description=\"A simple demo addition app\",\n",
    "    requirements=requirements,\n",
    "    extra_packages=['./src','./crew_ai_app.py'],\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "dztSGP0Z27D2"
   },
   "source": [
    "Now the reasoning engine is deployed. You can access your reasoning engine in the future using the following reasource name:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "2WY5dWAL2xga",
    "outputId": "5c4f334f-42e1-4b49-e2ff-e169f416d1c1"
   },
   "outputs": [],
   "source": [
    "print(reasoning_engine.resource_name)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "6POQNa4l4Xlh"
   },
   "source": [
    "Test if our Crew on reasoning engine instance can respond. Let's get a report on Henry VIII.\n",
    "You can rerun the CrewAI with different topics to see how the Agents respond. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "4OcdCwU_2QAv",
    "outputId": "b5abdba7-6d6c-4549-e6a3-0ef961fdfc74"
   },
   "outputs": [],
   "source": [
    "response = reasoning_engine.query(question=\"Henry VIII\")\n",
    "print(response)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "xAK_uoDf3uJG"
   },
   "source": [
    "# Cleanup\n",
    "If you wish to delete the deployment from reasoning engine simply uncomment and run the following cell"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "3NMC4L350Ey3",
    "outputId": "4914d43c-2aba-4961-f546-076fc2811bbe"
   },
   "outputs": [],
   "source": [
    "#reasoning_engine.delete()"
   ]
  }
 ],
 "metadata": {
  "colab": {
   "provenance": []
  },
  "environment": {
   "kernel": "conda-base-py",
   "name": "workbench-notebooks.m128",
   "type": "gcloud",
   "uri": "us-docker.pkg.dev/deeplearning-platform-release/gcr.io/workbench-notebooks:m128"
  },
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "conda-base-py"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.16"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
